/*
 * This class is used to store and retrieve the all Tasks that user entered  
 * This class supports the following commands:
                                               add;
											   delete;
											   search by key words;
											   display by taks type;
											   display by dates;
											   mofify;
											   undo;
											   mark Complete;
 */
#include "Storage.h"

const QString Storage::MESSAGE_EMPTY_STORAGE = "Storage database is empty. ";
const QString Storage::MESSAGE_CONFLICTING_MEETING = "Note, there is a conflict during this time period. ";
const string Storage::MESSAGE_FLOATING = "%s: %s. ";
const string Storage::MESSAGE_DEADLINE = "%s: %s by %s %s. ";
const string Storage::MESSAGE_MEETING = "%s: %s on %s from %s to %s. ";
const string Storage::MESSAGE_EVENT = "%s: %s on %s to %s, from %s to %s. ";

Storage::Storage() {
    storageLogger.setFileName(FILE_NAME_STORAGE_LOGGER);
}

Storage::~Storage() {}

// This operation is used to find the correct position of the task by comparing their
// dates and time, so that the tasks are stored in sequence
// 
// @param Task
//            is returned from TaskManager to Logic ; Logic to Storage.
void Storage::storeTask(Task addTask) {
	QVector<Task>::iterator iter = taskStorage.begin();

	if (addTask.getTaskType() == Task::FLOATING) {
		taskStorage.push_back(addTask);
	}
	else {
		while ((iter != taskStorage.end()) && 
			  ((*iter).getTaskType() != Task::FLOATING) && 
			  compareDateAndTime((*iter),addTask)) {
			iter++;
		}
		taskStorage.insert(iter,addTask);
	}
}


// This operation is used to compare the date and time of two tasks
//
//@param storedTask, addTask
//           one is the task need to be added,one is the task from the storage
// @return true  if Task1's date and time is earlier than Task2' date and time
//        false if Task1's date and time is later than Task2' date and time
bool Storage::compareDateAndTime(Task storedTask, Task addTask) {
	QDate storedDate, addDate;
	QTime storedTime, addTime;

	extractTaskParam(storedTask, storedDate, storedTime);
	extractTaskParam(addTask, addDate, addTime);

	if (storedDate < addDate) {
		return true;
	}
	else {
		if (storedTime <= addTime) {
			return true;
		}
	}

	return false;
}

//Function extracts the relevant task date parameters for comparison.
//Does not guard against Floating tasks.
void Storage::extractTaskParam(Task task, QDate &date, QTime &time) {
	Task::TaskType type = task.getTaskType();
	
	switch (type) {
		case Task::EVENT: 
		case Task::MEETING:
			date = task.getStartDate();
			time = task.getStartTime();
			return;

		case Task::DEADLINE:
			date = task.getEndDate();
			time = task.getEndTime();
			return;

		case Task::TaskType::FLOATING:
		default:
			throw InvalidInputException("unrecognized error.");
	};
}

// @param Task
//        is returned from TaskManager to Logic ; Logic to Storage.
//@return string of command feedback
void Storage::compileUserFeedback(CommandType command, Task feedbackTask, QString &userFeedback) {
	
	if (command == UNDO) {
		command = history.back();
	}

	switch(command) {
		case ADD:
			userFeedback = collectTaskDetails(MESSAGE_ADDED, feedbackTask);
			return;

		case DELETE:	
			userFeedback = collectTaskDetails(MESSAGE_DELETED, feedbackTask);
			return;

		case MODIFY:
			userFeedback = collectTaskDetails(MESSAGE_MODIFIED, feedbackTask);
			return;

		case COMPLETE:
			userFeedback = collectTaskDetails(MESSAGE_COMPLETED, feedbackTask);
			return;

		case DISPLAY:
		case CLEAR: 
		case SAVE: 
		case SEARCH:
		case EXIT:
		case INVALID: return;

		default:
			throw InvalidInputException("Unrecognized error");
	};
}


// This operation is used to task details depending on the type of task added 
// @param Task     
// @return string of task title, date, time depending on task type

QString Storage::collectTaskDetails(string command, Task task) {
	QString title = task.getTitle();

	if (task.getTaskType() == Task::FLOATING) {
		sprintf_s(buffer, MESSAGE_FLOATING.c_str(), command.c_str(), title.toStdString().c_str());
	}

	if(task.getTaskType() == Task::DEADLINE) {
		QString date = task.getEndDate().toString();
		QString endTime = task.getEndTime().toString();
		sprintf_s(buffer,
			      MESSAGE_DEADLINE.c_str(),
				  command.c_str(),
			      title.toStdString().c_str(),
				  date.toStdString().c_str(),
				  endTime.toStdString().c_str());
	}
	
	if(task.getTaskType() == Task::MEETING) {
		QString date = task.getEndDate().toString();
		QString startTime = task.getStartTime().toString();
		QString endTime = task.getEndTime().toString();
		sprintf_s(buffer,
			      MESSAGE_MEETING.c_str(),
				  command.c_str(),
				  title.toStdString().c_str(),
				  date.toStdString().c_str(),
				  startTime.toStdString().c_str(),
				  endTime.toStdString().c_str());
	}

	if(task.getTaskType() == Task::EVENT) {
		QString startDate = task.getStartDate().toString();
		QString endDate = task.getEndDate().toString();
		QString startTime = task.getStartTime().toString();
		QString endTime = task.getEndTime().toString();
		sprintf_s(buffer,
			      MESSAGE_EVENT.c_str(),
				  command.c_str(),
				  title.toStdString().c_str(),
				  startDate.toStdString().c_str(),
				  endDate.toStdString().c_str(),
				  startTime.toStdString().c_str(),
				  endTime.toStdString().c_str());
	}

	QString feedback(buffer);
	return feedback;
}


// This operation is used to store the tasks returned from logic in sequence of date and time
// If the incoming task is meetingTask, it will check whether the period of time for incoming 
// meeting task is available ,if not return a string if it is available, it will be stored.
// @param Task       
// @return string of command feecback
QString Storage::addCommand(Task addTask) {
	
	//Sets the flags required for undo
	history.push_back(ADD);
	storeLastTask.push_back(addTask);
	
	QString userFeedback;
	compileUserFeedback(ADD, addTask, userFeedback);

    if (((addTask.getTaskType() == Task::MEETING) || (addTask.getTaskType() == Task::EVENT)) 
		&& (!isConflict(addTask))) {
        userFeedback = userFeedback + MESSAGE_CONFLICTING_MEETING;
	}

    storageLogger.logInfo(userFeedback.toStdString());
	storeTask(addTask);
	
	return userFeedback;
}


//This operation is used to delete the Task by the task reference number
//@param reference Number
// @return a string of command feedback 

QString Storage::deleteCommand(int taskNum) {

	int currStorageSize = taskStorage.size();
	if (currStorageSize == 0) {
		return MESSAGE_EMPTY_STORAGE;
	}
	
    int foundTask = searchTaskIndex(taskNum);
    Task deleteTask = taskStorage[foundTask];
    assert(deleteTask.getTaskRef() == taskNum);

    for (int index = foundTask ; index < taskStorage.size()-1; index++){
		taskStorage[index] = taskStorage[index + 1];
    }
    
	taskStorage.pop_back();

	//Sets flags required for undo
	history.push_back(DELETE);
	storeLastTask.push_back(deleteTask);	
	
	QString userFeedback;
	compileUserFeedback(DELETE, deleteTask, userFeedback);

	return userFeedback;
}


//This operation is used to display all tasks which will be in the sequence of date and time
// @param 
//@return all the tasks in a vector

QVector<Task> Storage::displayAll() {
	for (int index = 0; index < taskStorage.size(); index++){
		taskStorage[index].updateTaskStatus();
	}

	return taskStorage;
}


//This operation determines which of the supported component types the user wants to perform
// @param ComponentTypeString
//          is the key word of the user command

Storage::Component Storage::determineComponent(QString component){

	if (util.equalsIgnoreCase(component, "title")) {
		return TITLE;
	}

	if (util.equalsIgnoreCase(component, "startdate")) {
		return STARTDATE;
	}

	if (util.equalsIgnoreCase(component, "enddate")) {
		return ENDDATE;
	} 

	if (util.equalsIgnoreCase(component, "starttime")) {
		return STARTTIME;
	}

	if (util.equalsIgnoreCase(component, "endtime")) {
		return ENDTIME;
	}

	if (util.equalsIgnoreCase(component, "tags")) {
		return TAGS;
	}

	return INVALIDCOMPONENT;
}

// This operation modifies the component of a task with a specific reference number,
// that the user wants to perform
// @param reference number; the componentType, content to change
// @return command feedback string
// No guard as the reference number given by the UI will always be a valid number
QString Storage::modifyCommand(int refNum, QString component, QString modifiedContent) {
    
	if (taskStorage.size() == 0) {
		return MESSAGE_EMPTY_STORAGE;
	}

	//Parser parser;
    int foundTask = searchTaskIndex(refNum);
	QDate getDate;
	QTime getTime;

	Task modifyTask = taskStorage[foundTask];
	taskStorage.remove(foundTask);

	//Set flags required for undo
	history.push_back(MODIFY);
	storeLastTask.push_back(modifyTask);

    Component componentType = determineComponent(component);
    switch (componentType) {

		case TITLE:
			modifyTask.setTitle(modifiedContent);
			break;

		case STARTDATE:
			getDate = parser.parseDate(modifiedContent);
			modifyTask.setStartDate(getDate);
			break;

		case ENDDATE:
			getDate = parser.parseDate(modifiedContent);
			modifyTask.setStartDate(getDate);
			break;

		case STARTTIME:
			getTime = parser.parseTime(modifiedContent);
			modifyTask.setStartTime(getTime);
			break;

		case ENDTIME:
			getTime = parser.parseTime(modifiedContent);
			modifyTask.setEndTime(getTime);
			break;

		case INVALIDCOMPONENT:
			storageLogger.logInfo("unable to parse command, invalid component entered");
			throw InvalidInputException("unable to parse command, invalid component entered. ");
			break;
    
		case TAGS:
			break;

		default:
        storageLogger.logInfo("unrecognized error. ");
			throw InvalidInputException("unrecognized error. ");
			break;
	};

	storeTask(modifyTask);

	QString userFeedback;
	compileUserFeedback(MODIFY, modifyTask, userFeedback);

	return userFeedback;
}

//This operation undo the last command performed by the user
//that the user wants to perform
//@return command feedback of the previous action

QString Storage::undoCommand() {
    QString userFeedback;
	int taskNum;

	switch (history.back()) {
		case ADD:
			taskNum = storeLastTask.back().getTaskRef();
			userFeedback = deleteCommand(taskNum);
			break;

		case DELETE:
			userFeedback = addCommand(storeLastTask.back());
			break;

		case MODIFY:
			userFeedback = undoModify();
			break;

		case COMPLETE:
			userFeedback = undoComplete();
			break;
	}

	history.pop_back();
	storeLastTask.pop_back();

	return userFeedback;
}


//This operation enables undo the last undo action
QString Storage::undoModify() {
	Task undo = storeLastTask.back();
	storeTask(undo);

	QString userFeedback;
	compileUserFeedback(MODIFY, undo, userFeedback);
	
	return userFeedback;
}

QString Storage::undoComplete() {
	Task undo = storeLastTask.back();
	undo.unmarkComplete();
	storeTask(undo);

	QString userFeedback;
	compileUserFeedback(ADD, undo, userFeedback);
	
	return userFeedback;
}

// This operation search for tasks based on the keyword given by the user
// @para keyword
// @return all the tasks or an exeption

QVector<Task> Storage::searchCommand(QString keyword) {
	QVector<Task> tasksFound = taskWithKeyword(keyword);
	return tasksFound;
}

QVector<Task> Storage::taskWithKeyword(QString keyword) {
	QVector<Task> tasksFound;
	
	for (int i = 0; i < taskStorage.size(); i++) {
		QString taskTitle = taskStorage[i].getTitle();
		
		//ignore case search
		std::transform(taskTitle.toStdString().begin(), 
					   taskTitle.toStdString().end(), 
					   taskTitle.toStdString().begin(), ::tolower);
		std::transform(keyword.toStdString().begin(), 
					   keyword.toStdString().end(), 
					   keyword.toStdString().begin(), ::tolower);

		if (taskTitle.contains(keyword)) {
			tasksFound.push_back(taskStorage[i]);
		}
	}

	return tasksFound;
}

QVector<Task> Storage::searchTagsCommand(QString tag) {
	QVector<Task> tasksFound = taskWithTags(tag);
	return tasksFound;
}

QVector<Task> Storage::taskWithTags(QString tag) {
	QVector<Task> tasksFound;
	
	for (int i = 0; i < taskStorage.size(); i++) {
		if (!taskStorage[i].getAllTags().isEmpty()) {
		QVector<QString> taskTag = taskStorage[i].getAllTags();
		
			for (int j = 0; j < taskTag.size(); j++) {
				//ignore case search
				std::transform(taskTag[j].toStdString().begin(), 
							   taskTag[j].toStdString().end(), 
							   taskTag[j].toStdString().begin(), ::tolower);
				std::transform(tag.toStdString().begin(), 
							   tag.toStdString().end(), 
							   tag.toStdString().begin(), ::tolower);

				if (taskTag[j] == tag) {
					tasksFound.push_back(taskStorage[i]);
				}
			}
		}
	}

	return tasksFound;
}

//This operation search for tasks based on the reference number of that tack
// @para reference number
//@return task with this specific reference number 
int Storage::searchTaskIndex(int refNum) {
	//assertion is not added as for all reference numbers passed from UI will be found in storage
	for (int i = 0; i < taskStorage.size(); i++) {
		if (taskStorage[i].getTaskRef() == refNum) {
			return i;
		}
	}
}


//This operation display all the tasks with a specific date given by the user
//@para month,day
//@return all the tasks with this date
//exeption is thrown when all the tasks are floating tasks with no dates

QVector<Task> Storage::displayDate(QDate date) {
	QVector<Task> foundTasks;
	int month = date.month(), day = date.day();

    for (int i = 0; i < taskStorage.size(); i++) {
	   	if (taskStorage[i].getTaskType() != Task::FLOATING) {
	    	QDate taskDate = taskStorage[i].getStartDate();		
	    	if ((taskDate.month() == month) && (taskDate.day() == day)) {
				taskStorage[i].updateTaskStatus();
	   			foundTasks.push_back(taskStorage[i]);
    		}
    	}
	}

	return foundTasks;
}


//This operation display all the floating tasks
//@return all the floating tasks
//exeption is thrown when there is no floating tasks
QVector<Task> Storage::displayFloating() {
	QVector<Task> floatingTasks;
	
	for(int i = 0; i < taskStorage.size(); i++) {
		if (taskStorage[i].getTaskType() == Task::FLOATING) {
			floatingTasks.push_back(taskStorage[i]);
		}
	}

	return floatingTasks;
}

//This operation display all the event tasks
// @return all the meeting tasks
//exeption is thrown when there is no meeting tasks
QVector<Task> Storage::displayEvent() {
	QVector<Task> meetingTasks;

	for (int i = 0; i < taskStorage.size(); i++) {
		if (taskStorage[i].getTaskType() == Task::EVENT) {
			taskStorage[i].updateTaskStatus();
			meetingTasks.push_back(taskStorage[i]);
		}
	}

	return meetingTasks;
}


//This operation display all the meeting tasks
// @return all the meeting tasks
//exeption is thrown when there is no meeting tasks
QVector<Task> Storage::displayMeeting() {
	QVector<Task> meetingTasks;

	for (int i = 0; i < taskStorage.size(); i++) {
		if (taskStorage[i].getTaskType() == Task::MEETING) {
			taskStorage[i].updateTaskStatus();
			meetingTasks.push_back(taskStorage[i]);
		}
	}

	return meetingTasks;
}


//This operation display all the deadline tasks
//@return all the deadline tasks
//exeption is thrown when there is no deadline tasks
QVector<Task> Storage::displayDeadline(){
	QVector<Task> deadlineTasks;
	
	for (int i=0;i<taskStorage.size();i++) {
		if(taskStorage[i].getTaskType() == Task::DEADLINE) {
			taskStorage[i].updateTaskStatus();
			deadlineTasks.push_back(taskStorage[i]);
		}
	}

	return deadlineTasks;
}


//This operation checks whether the incoming meeting task will conflict with an 
//existing meeting task in the same time period
//@return addTask
bool Storage::isConflict(Task addTask){
	QDateTime startDT, endDT, currStartDT, currEndDT;

	startDT = addTask.getStartDateTime();
	endDT = addTask.getEndDateTime();

	QVector<Task> currDate = displayDate(addTask.getStartDate());
	
	for (int i = 0; i < currDate.size(); i++) {
		if ((currDate[i].getTaskType() == Task::MEETING) || (currDate[i].getTaskType() == Task::EVENT)) {
			currStartDT = addTask.getStartDateTime();
			currEndDT = addTask.getEndDateTime();

			if (!((startDT > currEndDT)  ||  (endDT < currStartDT) )) {
				   return false;
			}
		}
	}
		
	return true;
}

// This operation is used to change the status of task to complete give task reference number
// @param task Reference number
// @return a string of completing the task

QString Storage::markComplete(int refNum) {
	int foundIndex = searchTaskIndex(refNum);
	taskStorage[foundIndex].markComplete();
	Task completeTask = taskStorage[foundIndex];

	QString userFeedback;
	compileUserFeedback(COMPLETE, completeTask, userFeedback);
	
	return userFeedback;
}

// This operation is used to return all the overdue tasks
//
// @return a vector of task of all overdue tasks
QVector<Task> Storage::checkOverdue() {
	QVector<Task> taskFound;

	for (int i = 0 ; i < taskStorage.size() ; i++) {
		taskStorage[i].updateTaskStatus();

		if (taskStorage[i].getTaskStatus() == Task::OVERDUE){
			taskFound.push_back(taskStorage[i]);
		}
	}

	return taskFound;
}

// This operation is used to return all the approaching tasks
//                                           tasks whose deadline is within 3 days from now
//these tasks will be sorted according to their level of urgencey
// @return a vector of task of all approaching tasks
QVector<Task> Storage::checkApproaching() {
	QVector<Task> taskFound;
	int app1 = 0, app2 = 0, app3 = 0;

	for (int i = 0; i < taskStorage.size() ; i++) {
		taskStorage[i].updateTaskStatus();

		if (taskStorage[i].getTaskStatus() == Task::APPROACH1) {
			taskFound.insert(app1, taskStorage[i]);
			app1++;
			app2++;
			app3++;
		}

		if (taskStorage[i].getTaskStatus() == Task::APPROACH2) {
			taskFound.insert(app2, taskStorage[i]);
			app2++;
			app3++;
		}

		if (taskStorage[i].getTaskStatus() == Task::APPROACH3) {
			taskFound.insert(app3, taskStorage[i]);
			app3++;
		}
	}

	return taskFound;
}